/**
 * @desc 解决浮动运算问题，避免小数点后产生多位数和计算精度损失。
 *
 * 问题示例：2.3 + 2.4 = 4.699999999999999，1.0 - 0.9 = 0.09999999999999998
 */

import { BaseType } from '../index';


export type NumberType = BaseType | null | undefined

/**
 * Correct the given number to specifying significant digits.
 *
 * @param n The input number
 * @param precision An integer specifying the number of significant digits
 *
 * @example strip(0.09999999999999998) === 0.1 // true
 */
function strip(n: NumberType, precision = 15): number {
  return +parseFloat(Number(n).toPrecision(precision));
}

/**
 * Return digits length of a number.
 *
 * @param b The input number
 */
function digitLength(b: BaseType): number {
  const eSplit = b.toString().split(/[eE]/);
  const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
  return len > 0 ? len : 0;
}

/**
 * Convert the given number to integer, support scientific notation.
 * The number will be scale up if it is decimal.
 *
 * @param n The input number
 */
function floatToFixed(n: BaseType): number {
  if (n.toString().indexOf('e') === -1) {
    return Number(n.toString().replace('.', ''));
  }
  const dLen = digitLength(n);
  return dLen > 0 ? strip(Number(n) * Math.pow(10, dLen)) : Number(n);
}


let boundaryCheckingState = true;

/**
 * Whether to check the bounds of number, default is enabled.
 *
 * @param bol The value to indicate whether is enabled
 */
function setBoundaryChecking(bol = true) {
  boundaryCheckingState = bol;
}

/**
 * Log a warning if the given number is out of bounds.
 *
 * @param n The input number
 */
function checkBoundary(n: number) {
  if (boundaryCheckingState) {
    if (n > Number.MAX_SAFE_INTEGER || n < Number.MIN_SAFE_INTEGER) {
      console.warn(`${n} is beyond boundary when transfer to integer, the results may not be accurate`);
    }
  }
}

/**
 * Create an operation to support rest params.
 *
 * @param operation The original operation
 */
function createOperation(operation: (n1: NumberType, n2: NumberType) => number): (...nums: NumberType[]) => number {
  return (...nums: NumberType[]) => {
    const [first, ...others] = nums;
    return others.reduce((prev, next) => operation(prev, next), first) as number;
  };
}

/**
 * Accurate multiplication.
 *
 * @param nums The numbers to multiply
 */
const multiply = createOperation((n1, n2) => {
  n1 = n1 || 0
  n2 = n2 || 0
  const num1Changed = floatToFixed(n1);
  const num2Changed = floatToFixed(n2);
  const baseNum = digitLength(n1) + digitLength(n2);
  const leftValue = num1Changed * num2Changed;

  checkBoundary(leftValue);

  return leftValue / Math.pow(10, baseNum);
});

/**
 * Accurate addition.
 *
 * @param nums The numbers to add
 */
const add = createOperation((n1, n2) => {
  n1 = n1 || 0
  n2 = n2 || 0
  // 取最大的小数位
  const baseNum = Math.pow(10, Math.max(digitLength(n1), digitLength(n2)));
  // 把小数都转为整数然后再计算
  return (multiply(n1, baseNum) + multiply(n2, baseNum)) / baseNum;
});

/**
 * Accurate subtraction.
 *
 * @param nums The numbers to subtract
 */
const subtract = createOperation((n1, n2) => {
  n1 = n1 || 0
  n2 = n2 || 0
  const baseNum = Math.pow(10, Math.max(digitLength(n1), digitLength(n2)));
  return (multiply(n1, baseNum) - multiply(n2, baseNum)) / baseNum;
});

/**
 * Accurate division.
 *
 * @param nums The numbers to divide
 */
const divide = createOperation((n1, n2) => {
  n1 = n1 || 0
  n2 = n2 || 0
  const n1Changed = floatToFixed(n1);
  const n2Changed = floatToFixed(n2);

  checkBoundary(n1Changed);
  checkBoundary(n2Changed);

  // fix: 类似 10 ** -4 为 0.00009999999999999999，strip 修正
  return multiply(n1Changed / n2Changed, strip(Math.pow(10, digitLength(n2) - digitLength(n1))));
});

/**
 * Accurate rounding method.
 *
 * @param n The number to round
 * @param decimal An integer specifying the decimal digits
 */
function round(n: NumberType, decimal: number): number {
  n = n || 0
  const base = Math.pow(10, decimal);
  let result = divide(Math.round(Math.abs(multiply(n, base))), base);
  if (Number(n) < 0 && result !== 0) {
    result = multiply(result, -1);
  }
  return result;
}

export { strip, add, subtract, multiply, divide, round, digitLength, floatToFixed, setBoundaryChecking };
